/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package at.priv.spindler.osgirunnner;

import at.priv.spindler.runner.tools.UnZip;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.maven.model.Dependency;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.codehaus.plexus.util.FileUtils;
import org.sonatype.aether.RepositorySystem;
import org.sonatype.aether.RepositorySystemSession;
import org.sonatype.aether.collection.CollectRequest;
import org.sonatype.aether.collection.DependencyCollectionException;
import org.sonatype.aether.graph.DependencyNode;
import org.sonatype.aether.repository.RemoteRepository;
import org.sonatype.aether.resolution.ArtifactRequest;
import org.sonatype.aether.resolution.ArtifactResolutionException;
import org.sonatype.aether.resolution.ArtifactResult;
import org.sonatype.aether.util.artifact.DefaultArtifact;

/**
 *
 * @author Stephan Spindler <stephan@spindler.priv.at>
 */
public abstract class AbstractOsgiRunnerMojo extends AbstractMojo {

    /**
     * @parameter default-value="${project}"
     */
    private org.apache.maven.project.MavenProject mavenProject;
    /**
     * The entry point to Aether, i.e. the component doing all the work.
     *
     * @component
     */
    private RepositorySystem repoSystem;
    /**
     * The current repository/network configuration of Maven.
     *
     * @parameter default-value="${repositorySystemSession}"
     * @readonly
     */
    private RepositorySystemSession repoSession;
    /**
     * The project's remote repositories to use for the resolution of plugins
     * and their dependencies.
     *
     * @parameter default-value="${project.remotePluginRepositories}"
     * @readonly
     */
    private List<RemoteRepository> remoteRepos;
    /**
     * The location where all files for running the osgi environment are put
     *
     * @parameter default-value="${project.build.directory}/osgi-runner/"
     */
    private String runnerDir;

    public void setRunnerDir(String runnerDir) {
        this.runnerDir = runnerDir.replace('/', File.separatorChar);
        this.felixRuntimeJar = this.runnerDir + "bin" + File.separator + "felix.jar";
        this.classpath = this.runnerDir + "lib";
    }
    /**
     * the bundles which sould be preinstalled into felix
     *
     * @parameter
     */
    private DeployBundle[] bundles;
    //public void setBundles
    
    /**
     * the extra deps which should be put on the classpath with felix
     * 
     * @parameter alias="classpathExtras"
     */
    private org.apache.maven.model.Dependency[] classpathExtras;
    
    
    /**
     * The location of the bundle list (all bundles in this list will be started
     * with the osgi framework) default:
     * ${project.directory}/src/main/bundles/list.xml
     *
     * not in use yet! parameter
     * default-value="${project.directory}/src/main/bundles/list.xml"
     */
    private String bundleList;
    
    /**
     * Property file for felix Runtime
     *
     * @parameter
     */
    private Properties felixProperties;
    /**
     * The felix-runtime
     *
     * @parameter alias="felix"
     */
    private DeployPackage felixRuntime;
    private String felixRuntimeJar;

    private Map<Integer, List<String>> bundleStartProperties = new HashMap<Integer, List<String>>();
    private Map<Integer, List<String>> bundleInstallProperties = new HashMap<Integer, List<String>>();
    //private final int defaultStartlevel = 20;
    
    private String classpath;
    
    private ArtifactResult resolveDependency(Dependency dep) throws MojoExecutionException {
        ArtifactRequest artifactRequest;
        ArtifactResult artifactResult;

        artifactRequest = new ArtifactRequest();
        artifactRequest.setArtifact(new DefaultArtifact(dep.getGroupId(), dep.getArtifactId(), dep.getType(), dep.getVersion()));

        artifactRequest.setRepositories( this.mavenProject.getRemoteProjectRepositories() );
        

        getLog().info("Resolving artifact " + artifactRequest.getArtifact().toString() + " from " + remoteRepos);

        try {
            artifactResult = this.repoSystem.resolveArtifact(repoSession, artifactRequest);

        } catch (ArtifactResolutionException e) {
            throw new MojoExecutionException(e.getMessage(), e);
        }

        getLog().info("Resolved artifact " + artifactResult.getArtifact().toString() + " to "
                + artifactResult.getArtifact().getFile() + " from "
                + artifactResult.getRepository());

        return artifactResult;
    }

    private void fetchFelixRuntime() throws MojoExecutionException {
        ArtifactResult artifactResult = resolveDependency(felixRuntime);

        getLog().info("Copy Artifact to " + felixRuntimeJar);
        artifactResult.getArtifact().getFile();

        try {
            FileUtils.copyFile(artifactResult.getArtifact().getFile(), new File(felixRuntimeJar));

        } catch (IOException ex) {
            String message = "IOError while copying felix runtime to " + felixRuntimeJar;
            getLog().error( message, ex);
            throw new MojoExecutionException( message, ex );
        }
        

        if (this.classpathExtras != null) {
            for (Dependency dep : this.classpathExtras) {
                artifactResult = resolveDependency(dep);
                String path = this.classpath + File.separator + artifactResult.getArtifact().getFile().getName();
                try {
                    FileUtils.copyFile(artifactResult.getArtifact().getFile(), new File(path));


                } catch (IOException ex) {
                    String message = "IO Error while copying lib " + artifactResult.getArtifact() + " to " + path;
                    getLog().error(message, ex);
                    throw new MojoExecutionException(message, ex);
                }
            }

        }
   

    }
    Thread shutdownHook = new Thread() {
        @Override
        public void run() {
            //TODO: cleanup stuff
        }
    };

    private void fetchBundles() throws MojoExecutionException {
        if (this.bundles == null) {
            return;
        }
        for (DeployBundle db : this.bundles) {
            ArtifactResult result = resolveDependency(db);
            File f = result.getArtifact().getFile();
            try {
                String path = this.runnerDir + File.separator + "startbundle" + File.separator + f.getName();
                getLog().info("Copy " + db + " to " + path);
                File toFile = new File( path );
                FileUtils.copyFile(f, toFile );
                //int n = db.getStartlevel();

                if( db.getStartlevel() > 0 ){
                    List<String> bundlestart = ( List<String> ) bundleStartProperties.get( db.getStartlevel() );
                    if( bundlestart == null ){
                        bundlestart = new ArrayList<String>();
                        bundleStartProperties.put( db.getStartlevel(), bundlestart);
                    }
                    URL url = new URL(new URL("file:"), "." + File.separator + "startbundle" + File.separator + f.getName() );
                   
                    try {
                        bundlestart.add( url.toURI().toString() );
                    } catch (URISyntaxException ex) {
                        getLog().error( "URI exception", ex );
                    }
                }
                if( db.getInstalllevel() > 0 ){
                    List<String> bundleInstall = ( List<String> ) bundleInstallProperties.get( db.getInstalllevel() );
                    if( bundleInstall == null ){
                        bundleInstall = new ArrayList<String>();
                        bundleInstallProperties.put( db.getInstalllevel(), bundleInstall);
                    }
                    URL url = new URL(new URL("file:"), "." + File.separator +  "startbundle" + File.separator + f.getName());
                    try {
                        bundleInstall.add( url.toURI().toString() );
                    } catch (URISyntaxException ex) {
                        getLog().error( "URI exception", ex );
                    }
                }
                //this.bundleStartProperties.put(n, bundlestart );
                
                
                
                //copyDep(db, new File(this.runnerDir + File.separator + "bundle"));

            } catch (IOException ex) {
                String message = "IO Exception occured: error during copy";
                getLog().error(message, ex);
                throw new MojoExecutionException(message, ex);
            }
        }
    }

    /**
     * @param d - the dependency
     * @param dir - the directory where to put resolved deps
     *
     * @throws MojoExecutionException also copy the dependencies of artifact
     *
     */
    private void copyDep(Dependency d, File dir) throws MojoExecutionException {

        org.sonatype.aether.graph.Dependency dep =
                new org.sonatype.aether.graph.Dependency(
                new DefaultArtifact(d.getGroupId() + ":" + d.getArtifactId() + ":" + d.getType() + ":" + d.getVersion()), null);


        CollectRequest collectRequest = new CollectRequest();
        collectRequest.setRoot(dep);
        //collectRequest.addRepository( this.remoteRepos );
        collectRequest.setRepositories(remoteRepos);
        try {
            DependencyNode node = repoSystem.collectDependencies(repoSession, collectRequest).getRoot();
            recursiveDepWalk(node, dir);

        } catch (DependencyCollectionException ex) {
            Logger.getLogger(AbstractOsgiRunnerMojo.class.getName()).log(Level.SEVERE, null, ex);
        }

    }

    private void recursiveDepWalk(DependencyNode node, File dir) throws DependencyCollectionException {

        ArtifactRequest request;
        ArtifactResult result;


        for (DependencyNode n : node.getChildren()) {
            recursiveDepWalk(n, dir);
        }
        //getLog().info("Dependency found:"  + node.getDependency() );
        //if (node.getDependency().getArtifact().getExtension().equalsIgnoreCase("ear")) {
        String path = "";
        try {
            getLog().info("Found Dependency: " + node.getDependency());

            request = new ArtifactRequest();

            request.setArtifact(node.getDependency().getArtifact());
            request.setRepositories(remoteRepos);

            result = repoSystem.resolveArtifact(repoSession, request);
            getLog().info("\tResolved deployPackage:\t " + result.getArtifact().toString() + " to "
                    + result.getArtifact().getFile() + " from "
                    + result.getRepository());

            //deployArtifact(result.getArtifact());
            File f = result.getArtifact().getFile();
            path = dir.getAbsolutePath() + File.separator + f.getName();
            FileUtils.copyFile(f, new File(path));
        } catch (IOException ex) {
            getLog().error("error while copying artifact to path: " + path );
        } catch (ArtifactResolutionException ex) {
            getLog().error("Artifact resolution failed", ex);
        }
        //}
    }

    protected void start() throws MojoExecutionException {

        getLog().info("props:");
        for (Object key : felixProperties.keySet()) {
            getLog().info("prop: " + key + ":" + felixProperties.getProperty(key.toString()));
        }




        fetchBundles();

        //store properties
        for( Map.Entry<Integer, List<String>> item  : this.bundleInstallProperties.entrySet() ){
            String prop = "";
            for( String s : item.getValue() ){
               prop = prop + s + " ";
            }
            //prop = prop.substring(0, prop.length() - 1 );
            //felixProperties.put("felix.auto.install." + item.getKey(), prop);
            felixProperties.put("felix.auto.install." + item.getKey(), prop);
        }
        
        
        for( Map.Entry<Integer, List<String>> item  : this.bundleStartProperties.entrySet() ){
            String prop = "";
            for( String s : item.getValue() ){
               prop = prop + s + " ";
            }
            //prop = prop.substring(0, prop.length() - 1 );
            //felixProperties.put("felix.auto.install." + item.getKey(), prop);
            felixProperties.put("felix.auto.start." + item.getKey(), prop);
        }
        
        
        
        
        String path = this.runnerDir + "conf";
        File p = new File( path );
        p.mkdirs();
        path = path + File.separator + "config.properties";
        try {
            File f = new File( path );
            f.createNewFile();
            FileWriter writer = new FileWriter(f);
            felixProperties.store(writer, "");
            writer.close();
            
        } catch (IOException ex) {
            String message = "Error while writing felix properties to " + path;
            getLog().error(message, ex);
            throw new MojoExecutionException(message, ex);
        }
        
        
        getLog().info("felix file: " + felixRuntimeJar);

        //if felix runtime not downloaded: do it!
        File felix = new File(felixRuntimeJar);
        //if( ! felix.exists() ){
        fetchFelixRuntime();
        //}

        //TODO: fetch bundles

        //start felix:
        //Runtime.getRuntime().exec( );
        Runtime.getRuntime().addShutdownHook(shutdownHook);
        try {
            String cmd[] = {"java", "-classpath", this.classpath + File.separator + "*",  "-jar", felixRuntimeJar };
            List<String> listcmd = new ArrayList<String>();
            
            listcmd.addAll( Arrays.asList(cmd) );
  
            
            String message = "";
            for( String s : listcmd ){
                message = message + s + " ";
            }
            getLog().info("starting felix: " + message );

            Process proc = Runtime.getRuntime().exec( listcmd.toArray(cmd), null, new File(runnerDir));

            //ExecuteStream errorStream = new ExecuteStream(proc.getErrorStream());
            //ExecuteStream outputStream = new ExecuteStream(proc.getInputStream());


            ThreadedStreamCopy errorStream = new ThreadedStreamCopy(proc.getErrorStream(), System.err);
            ThreadedStreamCopy outputStream = new ThreadedStreamCopy(proc.getInputStream(), System.out);
            ThreadedStreamCopy inputStream = new ThreadedStreamCopy(System.in, proc.getOutputStream(), true);
            //System.in;
            //System.out;

            //IOUtils.copy( proc.getInputStream(), System.out );

            errorStream.start();
            outputStream.start();
            inputStream.start();

            boolean except = true;
            while (except) {
                try {
                    int ret = proc.waitFor();
                    except = false;
                } catch (InterruptedException ex) {
                    getLog().error("interrupted while waiting", ex);
                }
            }
            errorStream.setStop();
            outputStream.setStop();
            inputStream.setStop();

            getLog().info("Framework stopped");

        } catch (IOException ex) {
            getLog().error("except", ex);
        }



    }

    protected void stop() {
    }

    class ThreadedStreamCopy extends Thread {

        InputStream in;
        OutputStream out;
        boolean stop = false;
        boolean eof = false;

        /**
         * @param eof - send eof at InputStream end to OutputStream end?
         *
         */
        ThreadedStreamCopy(InputStream in, OutputStream out, boolean eof) {
            this.in = in;
            this.out = out;
            this.eof = eof;
        }

        ThreadedStreamCopy(InputStream in, OutputStream out) {
            this.in = in;
            this.out = out;
        }

        public void setStop() {
            this.stop = true;
        }

        @Override
        public void run() {
            try {

                char buffer[] = new char[2048];
                int read;

                InputStreamReader isr = new InputStreamReader(this.in);
                BufferedReader br = new BufferedReader(isr);
                String line;
                OutputStreamWriter osw = new OutputStreamWriter(this.out);
                BufferedWriter bw = new BufferedWriter(osw);



                while ((read = br.read(buffer)) != -1) {
                    bw.write(buffer, 0, read);
                    bw.flush();
                }

                if (eof) {  //close stream only if eof set 
                    bw.close();
                }
            } catch (IOException ioe) {
                getLog().error("IOException", ioe);
            }
        }
    }
}
